package com.swingson;

import com.swingson.componentprocessor.*;
import com.swingson.componentprocessor.impl.*;
import com.swingson.controller.Controller;
import com.swingson.exceptions.SwingsonException;
import com.swingson.jsonparser.JsonObject;
import com.swingson.jsonparser.JsonParser;
import com.swingson.layout.processors.DefaultLayoutProcessor;
import com.swingson.layout.processors.LayoutProcessor;
import com.swingson.layout.processors.MigLayoutProcessor;
import com.swingson.properties.PropertiesProcessorResolver;
import com.swingson.properties.impl.*;
import com.swingson.properties.impl.PropertyProcessors.BooleanPropertyProcessor;
import com.swingson.properties.impl.PropertyProcessors.CharPropertyProcessor;
import com.swingson.properties.impl.PropertyProcessors.DoublePropertyProcessor;
import com.swingson.properties.impl.PropertyProcessors.FloatPropertyProcessor;
import com.swingson.properties.impl.PropertyProcessors.IntegerPropertyProcessor;
import com.swingson.properties.impl.PropertyProcessors.LongPropertyProcessor;
import com.swingson.properties.impl.PropertyProcessors.ShortPropertyProcessor;
import com.swingson.properties.impl.PropertyProcessors.StringPropertyProcessor;
import com.swingson.properties.specificclass_impl.*;
import java.awt.*;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import javax.swing.*;
import net.miginfocom.swing.MigLayout;

/**
 * Keywords<br/>
 * previous - previous component container - container of current component
 * top_container - top container passed to createGui method
 */
public class SwingsonGuiBuilder {

    private static final ComponentProcessorResolver componentProcessorResolver = new ComponentProcessorResolver();
    private static final PropertiesProcessorResolver propertiesProcessorResolver = new PropertiesProcessorResolver();
    private static final Map<Class, LayoutProcessor> layoutProcessors = new HashMap<Class, LayoutProcessor>();

    static {
        layoutProcessors.put(MigLayout.class, new MigLayoutProcessor());
        layoutProcessors.put(void.class, new DefaultLayoutProcessor());
    }

    public static LayoutProcessor getLayoutProcessor(Container container) {
        if (container.getLayout() == null) {
            return layoutProcessors.get(void.class);
        }

        LayoutProcessor processor = layoutProcessors.get(container.getLayout().getClass());
        if (processor == null) {
            return layoutProcessors.get(void.class);
        }

        return processor;
    }

    public static ComponentProcessorResolver getComponentProcessorResolver() {
        return componentProcessorResolver;
    }

    public static PropertiesProcessorResolver getPropertiesProcessorResolver() {
        return propertiesProcessorResolver;
    }

    static {
        init();
    }

    private static void init() {
        try {
            componentProcessorResolver.addComponentProcessor("panel", new JPanelProcessor());
            componentProcessorResolver.addComponentProcessor("jpanel", new JPanelProcessor());
            componentProcessorResolver.addComponentProcessor("label", new JLabelProcessor());
            componentProcessorResolver.addComponentProcessor("radio", new JRadioButtonProcessor());
            componentProcessorResolver.addComponentProcessor("button", new JButtonProcessor());
            componentProcessorResolver.addComponentProcessor("popupMenu", new PopupMenuProcessor());
            componentProcessorResolver.addComponentProcessor("jmenu", new JMenuProcessor());
            componentProcessorResolver.addComponentProcessor("scroll", new JScrollPaneProcessor());
            componentProcessorResolver.addComponentProcessor("splitPane", new JSplitPaneProcessor());
            componentProcessorResolver.addComponentProcessor("\"jcomp\"onent098_kjh098\"", new JComponentProcessor());//just label that will not be typed by user
            propertiesProcessorResolver.addTypeProcessingProperty(new BooleanPropertyProcessor(), boolean.class, Boolean.class);
            propertiesProcessorResolver.addTypeProcessingProperty(new IntegerPropertyProcessor(), int.class, Integer.class);
            propertiesProcessorResolver.addTypeProcessingProperty(new FloatPropertyProcessor(), float.class, Float.class);
            propertiesProcessorResolver.addTypeProcessingProperty(new DoublePropertyProcessor(), double.class, Double.class);
            propertiesProcessorResolver.addTypeProcessingProperty(new LongPropertyProcessor(), long.class, Long.class);
            propertiesProcessorResolver.addTypeProcessingProperty(new ShortPropertyProcessor(), short.class, Short.class);
            propertiesProcessorResolver.addTypeProcessingProperty(new CharPropertyProcessor(), char.class, Character.class);
            propertiesProcessorResolver.addTypeProcessingProperty(new StringPropertyProcessor(), String.class);
            propertiesProcessorResolver.addTypeProcessingProperty(new ColorPropertyProcessor(), Color.class);
            propertiesProcessorResolver.addTypeProcessingProperty(new DimensionPropertyProcessor(), Dimension.class);

            propertiesProcessorResolver.addNameClassPropertyProcessor("ng-visible", JComponent.class, new NgVisiblePropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor("ng-text", JComponent.class, new NgTextPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor("ng-enabled", JComponent.class, new NgEnabledPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor("action", JTextField.class, new ActionComponentPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor("action", AbstractButton.class, new ActionComponentPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor("icon", AbstractButton.class, new IconPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor("action", JMenuItem.class, new ActionComponentPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor("hotkey", JMenuItem.class, new HotkeyPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor("id", JComponent.class, new IdPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor("x", JComponent.class, new XPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor("x2", JComponent.class, new XPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor("y", JComponent.class, new YPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor("y2", JComponent.class, new YPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor("width", JComponent.class, new WidthPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor("height", JComponent.class, new HeightPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor("mig", JComponent.class, new MigPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor("miglayoutConstraints", JComponent.class, new MigLayoutConstraintsPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor("titledBorder", JComponent.class, new TitleBorderPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor("onChanged", JSpinner.class, new OnChangeSpinnerPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor("onSelected", JComboBox.class, new OnChangeComboBoxPropertyProcessor(OnChangeComboBoxPropertyProcessor.ChangeType.SELECT));
            propertiesProcessorResolver.addNameClassPropertyProcessor("onDeselected", JComboBox.class, new OnChangeComboBoxPropertyProcessor(OnChangeComboBoxPropertyProcessor.ChangeType.DESELECT));
            propertiesProcessorResolver.addNameClassPropertyProcessor("onFileDrop", JComponent.class, new OnFileDropPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor("onException", JComponent.class, new OnUnhandledExceptionPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor("onPopup", JPopupMenu.class, new PopupShowPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor("onPopup", JMenu.class, new MenuShowPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor("separator", JPopupMenu.class, new PopupMenuSeparatorPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor("group", JRadioButton.class, new RadioGroupPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor(MouseActionPropertyProcessor.mouseClicked, JComponent.class, new MouseActionPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor(MouseActionPropertyProcessor.mouseDrag, JComponent.class, new MouseActionPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor(MouseActionPropertyProcessor.mouseEntered, JComponent.class, new MouseActionPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor(MouseActionPropertyProcessor.mouseExited, JComponent.class, new MouseActionPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor(MouseActionPropertyProcessor.mouseMove, JComponent.class, new MouseActionPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor(MouseActionPropertyProcessor.mousePressed, JComponent.class, new MouseActionPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor(MouseActionPropertyProcessor.mouseReleased, JComponent.class, new MouseActionPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor(MouseActionPropertyProcessor.mouseWheel, JComponent.class, new MouseActionPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor(KeyActionPropertyProcessor.keyPressed, JComponent.class, new KeyActionPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor(KeyActionPropertyProcessor.keyReleased, JComponent.class, new KeyActionPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor(KeyActionPropertyProcessor.keyTyped, JComponent.class, new KeyActionPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor(FocusListenerPropertyProcessor.focusGained, JComponent.class, new FocusListenerPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor(FocusListenerPropertyProcessor.focusLost, JComponent.class, new FocusListenerPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor(ComponentListenerPropertyProcessor.onHidden, JComponent.class, new ComponentListenerPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor(ComponentListenerPropertyProcessor.onMoved, JComponent.class, new ComponentListenerPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor(ComponentListenerPropertyProcessor.onResized, JComponent.class, new ComponentListenerPropertyProcessor());
            propertiesProcessorResolver.addNameClassPropertyProcessor(ComponentListenerPropertyProcessor.onShown, JComponent.class, new ComponentListenerPropertyProcessor());
        } catch (Exception ex) {
            ex.printStackTrace();
            throw new SwingsonException(ex);
        }
    }

    public static void addControl(Class clazz) {
        addControl(getComponentName(clazz), clazz);
    }

    public static void addControl(String name, Class clazz) {
        CustomComponentProcessor processor = new CustomComponentProcessor(clazz);
        componentProcessorResolver.addComponentProcessor(name, processor);
    }

    private static String getComponentName(Class clazz) {
        return clazz.getSimpleName();
    }

    public static SwingsonGui createGui(String json, Component container) {
        SwingsonGuiBuilder builder = new SwingsonGuiBuilder();
        try {
            return builder.createGuiInternal(new ByteArrayInputStream(json.getBytes("UTF-8")), container);
        } catch (UnsupportedEncodingException ex) {
            throw new SwingsonException(ex);
        }
    }

    /**
     * Create gui for <b>container</b> from json file. Name of the json file
     * should be equal to name of container class(MainJFrame.java -
     * MainJFrame.json)
     */
    public static SwingsonGui createGuiFromJsonInPackage(Component container) {
        String jsonName = container.getClass().getSimpleName();
        return createGuiFromJsonInPackage(jsonName + ".json", container);
    }

    public static SwingsonGui createGuiFromJsonInPackage(String jsonFile, Component container) {
        if (container == null) {
            throw new SwingsonException("Container should not be null");
        }
        InputStream stream = container.getClass().getResourceAsStream(jsonFile);
        if (stream == null) {
            throw new SwingsonException("Missing swingson gui json file " + jsonFile + " for " + container.getClass().getSimpleName() + " class");
        }
        SwingsonGuiBuilder builder = new SwingsonGuiBuilder();
        return builder.createGuiInternal(stream, container);
    }

    public static SwingsonGui createGui(InputStream jsonStream, Component container) {
        SwingsonGuiBuilder builder = new SwingsonGuiBuilder();
        return builder.createGuiInternal(jsonStream, container);
    }

    /**
     * stream that contains json in UTF-8 encoding with gui markup
     */
    public SwingsonGui createGuiInternal(InputStream jsonStream, Component container) {
        JsonParser parser = new JsonParser();
        JsonObject element;
        try {
            element = (JsonObject) parser.parse(new InputStreamReader(jsonStream, "UTF-8"));
            jsonStream.close();
        } catch (UnsupportedEncodingException ex) {
            throw new SwingsonException(ex);
        } catch (IOException ex) {
            throw new SwingsonException("Cannot read stream with json", ex);
        }

        SwingsonGui gui = new SwingsonGui();
        JComponent jcomp = getJComponentFromPassedComponent(container);
        jcomp.setLayout(new MigLayout());
        gui.setTopComponent(jcomp);
        gui.setControllerComponent(container);
        collectAllJComponentsFromTopComponent(container, gui);
        processComponent(element, jcomp, null, gui);
        fillControllersWithComponents(gui);
        return gui;
    }

    private void fillObjectWithComponents(Object obj, SwingsonGui gui) {
        Class clazz = obj.getClass();
        while (clazz != null) {
            for (Field f : clazz.getDeclaredFields()) {
                if (Utils.isChild(f.getType(), JComponent.class)) {
                    String fieldName = f.getName();
                    if (gui.getDefinedComponent(fieldName) != null) {
                        JComponent component = gui.getDefinedComponent(fieldName).getComponent();
                        f.setAccessible(true);
                        try {
                            Object value = f.get(obj);
                            if (value == null) {
                                f.set(obj, component);
                            }
                        } catch (Exception ex) {
                            throw new SwingsonException(ex);
                        }
                    }
                }
            }
            clazz = clazz.getSuperclass();
        }
    }

    private void fillControllersWithComponents(SwingsonGui gui) {
        for (Controller controller : gui.getControllers()) {
            controller.setSwingsonGui(gui);
            fillObjectWithComponents(controller, gui);
        }
        fillObjectWithComponents(gui.getControllerComponent(), gui);
    }

    private JComponent getJComponentFromPassedComponent(Component component) {
        if (component instanceof JComponent) {
            return (JComponent) component;
        }
        if (component instanceof JFrame) {
            return (JComponent) ((JFrame) component).getContentPane();
        }
        if (component instanceof JDialog) {
            return (JComponent) ((JDialog) component).getContentPane();
        }
        throw new SwingsonException("Swingson cannot process root component of type " + component.getClass().getSimpleName() + ". Component should be JFrame, JDialog or any class derived from JComponent");
    }

    private void processComponent(JsonObject element, JComponent currentComponent, JComponent parent, SwingsonGui gui) {
        JsonPropertiesProcessor processor = new JsonPropertiesProcessor();
        processor.processProperties(element, currentComponent, gui, parent);
    }

    //we should save all swing components already defined in top component for example tables, buttons, labels, text fields
    private void collectAllJComponentsFromTopComponent(Object component, SwingsonGui gui) {
        Class clazz = component.getClass();
        while (clazz != null) {
            for (Field f : clazz.getDeclaredFields()) {
                if (Utils.isChild(f.getType(), Controller.class)) {
                    f.setAccessible(true);
                    try {
                        Controller controller = (Controller) f.get(component);
                        if (controller == null) {
                            controller = (Controller) f.getType().newInstance();
                        }
                        gui.getControllers().add(controller);
                    } catch (Exception ex) {
                        throw new SwingsonException(ex);
                    }
                } else if (Utils.isChild(f.getType(), JComponent.class)) {
                    try {
                        f.setAccessible(true);
                        JComponent value = (JComponent) f.get(component);
                        if (value != null) {
                            gui.addDefinedComponent(f.getName(), f, value);
                            value.putClientProperty("tid", f.getName());
                        }
                    } catch (SecurityException | IllegalArgumentException | IllegalAccessException ex) {
                        throw new SwingsonException(ex);
                    }
                }
            }
            clazz = clazz.getSuperclass();
        }
    }

    public static void setWindowsLookAndFeel() {
        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
            JPopupMenu.setDefaultLightWeightPopupEnabled(false);
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
            throw new SwingsonException(ex);
        }
    }
}
